package my.actuate.beans;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.AbstractNamedMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.ManagementServletContext;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;

import my.spring.web.ModelAndView;
import my.util.Log;
import my.util.Response;

public class BeansMvcEndpoint extends AbstractNamedMvcEndpoint implements InitializingBean {
    static final String         PARAME_ACTION        = "action";
    static final String         PARAME_CONTEXTID     = "contextId";
    static final String         PARAME_PROPERTY_NAME = "propertyName";
    static final String         PARAME_METHOD_NAME   = "methodName";
    static final String         PARAME_BEAN_NAME     = "beanName";
    static final String         PARAME_NEWVALUE      = "newValue";
    static final String         PARAME_TYPE_NAME     = "typeName";
    static final String         URL_BEAN_CHANGE      = "/change";
    static final String         URL_BEAN_INVOKE      = "/invoke";
    protected final Log         log                  = Log.of(getClass());
    private String              beansUrlPrefix;
    private String              propertyChangeUrl;
    private String              methodInvokeUrl;
    /**
     * map<beanName, view>
     */
    private Map<String, String> beanViewMap;
    @Autowired
    ManagementServletContext    managementServletContext;
    @Autowired
    BeansService                beansService;



    public BeansMvcEndpoint() {
        super("admin/beans", "/admin/beans", false);
    }



    @Override
    public void afterPropertiesSet() throws Exception {
        String beansUrlPrefix = managementServletContext.getContextPath() + getPath();
        try {
            beansUrlPrefix = URI.create(beansUrlPrefix).normalize().toString();
        } catch (Exception e) {
            setEnabled(Boolean.FALSE); // TODO 应该没什么用. 起个提示作用.
            log.error(e, "path format not configure right. contextPath: {0.contextPath}, endpoint path: {1.path}", managementServletContext, this);
        }
        setBeansUrlPrefix(beansUrlPrefix);
        setPropertyChangeUrl(getBeansUrlPrefix() + URL_BEAN_CHANGE);
        setMethodInvokeUrl(getBeansUrlPrefix() + URL_BEAN_INVOKE);
    }



    @EventListener(ApplicationReadyEvent.class)
    void lazyInit(ApplicationReadyEvent pReadyEvent) {
        try {
            log.info("lazy init beansMvcEndpoint");
            customInit();
        } catch (Exception e) {
            log.error(e, "lazyInit(): error!");
        }
    }



    protected void customInit() {
        String[] beanNames = beansService.getApplicationContext().getBeanNamesForType(AbstractHandlerMethodMapping.class);
        if (Utils.isNotEmpty(beanNames)) {
            for (String beanName : beanNames) {
                getBeanViewMap().put(beanName, "bean/AbstractHandlerMethodMapping");
            }
        }
        beanNames = beansService.getApplicationContext().getBeanNamesForAnnotation(Configuration.class);
        if (Utils.isNotEmpty(beanNames)) {
            for (String beanName : beanNames) {
                getBeanViewMap().put(beanName, "bean/ConfigurationBean");
            }
        }
    }



    // @ExceptionHandler
//    String expHandler(Exception pEx, HttpServletRequest pReq) {
//        log.error(pEx, "BeansMvcEndpoint out error!!");
//        pReq.setAttribute("ex", pEx);
//        return "error";
//    }

    @GetMapping(path = {"", "/"}, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView browse(@RequestParam Map<String, Object> pParams) {
        String action = (String) pParams.get(PARAME_ACTION);
        log.trace("browse(): action: {0}", action);
        ModelAndView modelAndView = new ModelAndView();
        String viewName = "index";
        if ("searchBeans".equals(action)) {
            String beanName = (String) pParams.get(PARAME_BEAN_NAME);
            log.debug("searchBeans beanName: {0}", beanName);
            Map<String, Object> beans = beansService.searchBeans(beanName);
            modelAndView.addObject("beans", beans)
                .addObject("limitCount", beansService.getSearchBeanLimitCount())
                .addObject("kw", beanName);
            viewName = "search";
        } else if ("getSubtypes".equals(action)) {
            String typeName = (String) pParams.get(PARAME_TYPE_NAME);
            log.debug("getSubtypes typeName: {0}", typeName);
            List<Map<String, Object>> subTypes = beansService.searchSubtype(typeName);
            modelAndView.addObject("subtypes", subTypes);
            viewName = "fragments :: subtypes";
        } else {
            List<Class<?>> typeList = beansService.getTypeList();
            if (Utils.isNotEmpty(typeList)) {
                List<Map<String, Object>> subTypes = beansService.searchSubtype(typeList.get(0).getName());
                modelAndView.addObject("subtypes", subTypes);
            }
            modelAndView.addObject("types", typeList);
            modelAndView.addObject("contextId", beansService.getApplicationContext().getId());
            viewName = "index";
        }
        viewName = builViewName(viewName);
        log.trace("view name: {0}", viewName);
        modelAndView.setViewName(viewName);
        return modelAndView;
    }



    @GetMapping(path = {"/{beanName:.*}"}, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView browse(@PathVariable("beanName") String pBeanName, @RequestParam Map<String, Object> pParams) {
        log.debug("browse(): beanName: {0}, params: {1}", pBeanName, pParams);
        my.spring.web.ModelAndView modelAndView = new ModelAndView();
        String contextId = (String) pParams.get(PARAME_CONTEXTID);
        String propertyName = (String) pParams.get(PARAME_PROPERTY_NAME);
        String methodName = (String) pParams.get(PARAME_METHOD_NAME);
        String viewName = null;
        if (StringUtils.isNotBlank(propertyName)) {
            viewName = "property";
            Object propertyVO = beansService.buildPropertyVO(propertyName, pBeanName, contextId);
            modelAndView.addObject(propertyVO, true);
        } else if (StringUtils.isNotBlank(methodName)) {
            Map<String, Object> methodInfo = beansService.buildMethodInfo(methodName, pBeanName, contextId);
            modelAndView.addAllObjects(methodInfo);
            viewName = "method";
        } else {
            Object beanVO = beansService.buildBeanVO(pBeanName, contextId);
            modelAndView.addObject(beanVO, true);
            viewName = getBeanViewMap().get(pBeanName);
            if (Utils.isBlank(viewName)) {
                viewName = "bean";
            }
        }
        viewName = builViewName(viewName);
        modelAndView.setViewName(viewName);
        return modelAndView;
    }



    @ResponseBody
    @PostMapping(path = {URL_BEAN_CHANGE})
    public Response change(@RequestParam Map<String, Object> pParams) {
        log.debug("change(): params: {0}", pParams);
        String contextId = (String) pParams.get(PARAME_CONTEXTID);
        String propertyName = (String) pParams.get(PARAME_PROPERTY_NAME);
        String beanName = (String) pParams.get(PARAME_BEAN_NAME);
        String newValue = (String) pParams.get(PARAME_NEWVALUE);
        Response result = beansService.changeProperty(propertyName, beanName, contextId, newValue);
        return result;
    }



    @ResponseBody
    @PostMapping(path = {URL_BEAN_INVOKE})
    public Response invoke(@RequestParam Map<String, Object> pParams) {
        log.debug("invoke(): params: {0}", pParams);
        String methodName = (String) pParams.get(PARAME_METHOD_NAME);
        String beanName = (String) pParams.get(PARAME_BEAN_NAME);
        String contextId = (String) pParams.get(PARAME_CONTEXTID);
        Response result = beansService.invokeMethod(methodName, beanName, contextId);
        return result;
    }




    private String builViewName(String viewSuffix) {
        // String contextPath = managementServletContext.getContextPath();
        StrBuilder sb = new StrBuilder();
        // if (!contextPath.endsWith("/")) {
        // sb.append("/");
        // }
        sb.append("admin/beans/").append(viewSuffix);
        return sb.toString();

    }



    public String getBeansUrlPrefix() {
        return beansUrlPrefix;
    }



    private void setBeansUrlPrefix(String pBeanUrlPrefix) {
        beansUrlPrefix = pBeanUrlPrefix;
    }



    /**
     * @return the propertyChangeUrl
     */
    public String getPropertyChangeUrl() {
        return propertyChangeUrl;
    }



    /**
     * @param pPropertyChangeUrl the propertyChangeUrl to set
     */
    private void setPropertyChangeUrl(String pPropertyChangeUrl) {
        propertyChangeUrl = pPropertyChangeUrl;
    }



    /**
     * @return the beanViewMap
     */
    public Map<String, String> getBeanViewMap() {
        if (beanViewMap == null) {
            synchronized(this) {
                if (beanViewMap == null) {
                    beanViewMap = new HashMap<>();
                }
            }
        }
        return beanViewMap;
    }



    /**
     * @param pBeanViewMap the beanViewMap to set
     */
    public void setBeanViewMap(Map<String, String> pBeanViewMap) {
        beanViewMap = pBeanViewMap;
    }



    /**
     * @return the methodInvokeUrl
     */
    public String getMethodInvokeUrl() {
        return methodInvokeUrl;
    }



    /**
     * @param pMethodInvokeUrl the methodInvokeUrl to set
     */
    private void setMethodInvokeUrl(String pMethodInvokeUrl) {
        methodInvokeUrl = pMethodInvokeUrl;
    }
}
